---
title: "Abstract types & polymorphism"
sidebar_position: 11
---

import Mermaid from "@theme/Mermaid";

# Abstract types and polymorphism

GraphQL supports two abstract types suitable for usage in output: interfaces and
unions. An interface defines a list of fields; all objects that implement the
interface must implement fields compatible with these. A union is a simple list
of possible object types. At runtime, positions that return one of these
abstract types must resolve to a concrete object type; thus abstract types allow
GraphQL to describe positions where data polymorphism can occur.

Gra*fast* supports GraphQL interfaces and unions, both through traditional
GraphQL.js resolvers and through plan resolvers. Resolvers work the same
(basically) as they do in GraphQL.js so we won’t dig into them here, but let’s
look into how Gra*fast* supports abstract types via plan resolvers (which
enables greater efficiency!).

## Polymorphic positions

To make it easier to talk about planning a polymorphic GraphQL query, let’s
define the term “polymorphic position” to refer to a position in our GraphQL
operation whose return type is an abstract type (an interface or union).

Imagine you have a GraphQL schema such as:

```graphql {13,14}
interface Animal {
  name: String!
}
type Cat extends Animal {
  name: String!
  numberOfLives: Int!
}
type Dog extends Animal {
  name: String!
  wagsTail: Boolean!
}
type Query {
  bestAnimal: Animal
  randomAnimals: [Animal]
}
```

One query to this schema could be:

```graphql {2}
query BestAnimal {
  bestAnimal {
    name
    ... on Cat {
      numberOfLives
    }
    ... on Dog {
      wagsTail
    }
  }
}
```

Here the return type of the `bestAnimal` field is an abstract type (`Animal`,
which is an interface), so the return type of `bestAnimal` is a polymorphic
position in this query.

Another query could be:

```graphql {2}
query RandomAnimals {
  randomAnimals {
    name
    ... on Cat {
      numberOfLives
    }
    ... on Dog {
      wagsTail
    }
  }
}
```

Here the return type of `randomAnimals` is a list (`[Animal]`). A list is not
itself abstract, however the type inside the list is an abstract type
(`Animal` again), so a polymorphic position in this query is inside the list
returned by `randomAnimals`.

Operations may have any number (0 or higher) of polymorphic positions.

## Planning polymorphism

Planning a polymorphic position is a collaboration between the field’s plan
resolver and the abstract type’s `planType` method, which describes how to
resolve abstract values into their concrete type and associated data.

The step used as the result of a polymorphic position (e.g. returned from a
field plan resolver) represents a value we call the "specifier". The term
"specifier" is used because it does not actually have to be the object itself
(though it can be), it needs to be something that sufficiently describes that
item such that the abstract type can determine how to fetch the object.

Gra*fast* recognizes that sometimes you need to fetch an entity to figure out
what type it is, and sometimes you need to know what type it is in order to
fetch it (and sometimes it’s a little of both).

For example you might have an “animals” table in your database that details if
an individual record is a cat, dog, fish, budgie or similar. In this case you
need to fetch the record in order to determine the type of the value. We’ll call
this “fetch-to-type”, and for it we can have the specifier be the record’s
primary key, which we’ll call `id`.

An opposing example would be a GraphQL Node ID: here a string such as `User:1`
(but typically Base64 encoded) indicates the type up front, along with an
identifier, and you use this type to determine how to fetch the record. We’ll
call this “type-to-fetch”, and the specifier is this string we mentioned:
`User:1`.

{

<figure>
  <Mermaid
    chart={`
%%{init: {'themeVariables': { 'fontSize': '12px'}}}%%
  graph TD
subgraph NodeID["Node ID: &ldquo;type-to-fetch&rdquo;"]
v["$nodeId<br>(e.g. User:1 or Organization:2)"]
v-->|isUser?| GetUser[["Fetch user by id"]]
v-->|isOrganization?| GetOrg[["Fetch organization by id"]]
GetUser --> User
GetOrg --> Organization
end
subgraph Vets["Shared storage: &ldquo;fetch-to-type&rdquo;"]
v2["$animalId<br>(e.g. 1, 2, 3)"]
v2-->Animal[["Fetch animal by id"]]
Animal-->|isCat?| Cat
Animal-->|isDog?| Dog
end
`}
  />
  <figcaption>
    The difference in using “fetch-to-type” and “type-to-fetch”: In
    “fetch-to-type”, the record must be fetched before the type can be known; in
    “type-to-fetch”, the type is known up-front and the way the record is
    fetched depends on that.
  </figcaption>
</figure>
}

The specifier that a given abstract type requires is based on the expectations
of its `planType` method, which the schema author supplies.

### `planType`

If you use `makeGrafastSchema()` then you can specify `planType` on the
interface or union type:

```ts
const schema = makeGrafastSchema({
  typeDefs: /* GraphQL */ `
    interface Animal {
      id: ID!
      #...
    }
    union Entity = Animal | Alien
  `,
  interfaces: {
    Animal: {
      planType($specifier, info) {
        /* ... */
      },
      // Also: toSpecifier($step)
    },
  },
  unions: {
    Entity: {
      planType($specifier, info) {
        /* ... */
      },
      // Also: toSpecifier($step)
    },
  },
});
```

If you’re using GraphQL.js or another construction mechanism then the method
actually lives at `type.extensions.grafast.planType`:

```ts
const Animal = new GraphQLInterfaceType({
  name: "Animal",
  fields: {
    /* ... */
  },
  extensions: {
    grafast: {
      planType($specifier, info) {
        /* ... */
      },
      // Also: toSpecifier($step)
    },
  },
});
const Entity = new GraphQLUnionType({
  name: "Entity",
  types: [Animal /* , ... */],
  extensions: {
    grafast: {
      planType($specifier, info) {
        /* ... */
      },
      // Also: toSpecifier($step)
    },
  },
});
```

However you add it, `planType` accepts two parameters: the specifier step (the
combined data described above) and an `info` object (see
[“Advanced”](#advanced)). The function must return an `AbstractTypePlanner`
object, which is an object with two entries:

- `$__typename` (required): a step representing the name of the matching
  concrete object type.
- `planForType(t)` (optional): a function that is called for each possible
  concrete object type, `t`, and must return the step to use for that type (or
  `null` if creating such a step is not possible). If
  unspecified, the `$specifier` step will be used for all object types.

In our fetch-to-type example from before, `planType` might look like this:

```ts
function planType(
  $specifier: Step<number>,
  info: PlanTypeInfo,
): AbstractTypePlanner {
  // Fetch the database record for this $specifier
  const $record = animals.get({ id: $specifier });
  // Extract the type from the relevant column
  const $type = $record.get("type");
  // Convert that to a GraphQL type name
  const $__typename = lambda($type, animalTypeNameFromType, true);
  // Return our polymorphic type planner
  return {
    $__typename,
    planForType(t) {
      // All the different types are represented by data from the same resource
      return $record;
    },
  };
}

// Turns a database value such as "cat" into a GraphQL type name such as "Cat"
function animalTypeNameFromType(type: string) {
  return (
    { cat: "Cat", dog: "Dog", fish: "Fish", budgie: "Budgie" }[type] ?? null
  );
}
```

However, in our type-to-fetch example, `planType` would perform more logic
inside the `planForType` method, and needs less work to find the type name:

```ts
function planType(
  $specifier: Step<string>,
  info: PlanTypeInfo,
): AbstractTypePlanner {
  // Parse the NodeID into a typename and identifier
  const $parsed = lambda($specifier, parseNodeId, true);
  // Extract the type name:
  const $__typename = get($parsed, "__typename");
  // Return our polymorphic type planner
  return {
    $__typename,
    planForType(t) {
      // Each different type has its own plan:
      switch (t.name) {
        // This is the 'User' type, so extract the ID and fetch the user:
        case "User": {
          const $id = get($parsed, "id");
          return users.get({ id: $id });
        }
        // Resolving other types may work in similar or different ways:
        case "Organization": {
          const $id = get($parsed, "id");
          return organizations.get({ organizationId: $id });
        }
        default: {
          console.warn(`Don't know how to fetch ${t.name}`);
          return null;
        }
      }
    },
  };
}

function parseNodeId(nodeId: string) {
  const [__typename, rawId] = nodeId.split(":");
  const id = parseInt(rawId, 10);
  return { __typename, id };
}
```

Currently Gra*fast* will call `planForType` (if present) for each possible
object type at a given polymorphic position, and then will group these by the
ones returning the same (or equivalent) steps into a `polymorphicPartition` and
continue planning for each type from there.

:::info Not optimal enough? Get in touch!

Walking all possible object types is a simple approach, but it can inflate the
time spent planning an operation, especially for highly polymorphic operations.
We minimize this cost by fanning in before fanning out (see the below infobox),
but there’s space for more optimization: at some point, Gra*fast* might add
support for on-demand polymorphic planning.

With on-demand polymorphic planning, each “polymorphic branch” of the plan will
only be planned the first time that an object of that type is met at runtime.
This on-demand polymorphic planning strategy should significantly decrease
initial planning time for highly polymorphic operations, and may result in many
of the paths never needing to be planned at all. If this is something your
deployment of Gra*fast* needs, please get in touch.

:::

:::info Gra*fast* avoids exponential branching

Note the type-to-fetch example causes the operation plan to branch: `User` and
`Organization` each have different steps, and so we split them into separate
`polymorphicPartition` layer plans.

Excessive branching complicates both planning and execution and could lead to
denial of service. Gra*fast* made the fundamental choice to “fan in” before
“fanning out” in polymorphism to avoid exponential branching, and that choice
has had a significant impact on the shape of the APIs it makes available.

If planned naively, nested polymorphic positions could lead to significant
branching. If each polymorphic position could represent `P` different types and we queried
`D` levels deep then recursing through each different type at each
level would produce up to `P^D` different branches — the complexity would
scale exponentially with query depth. This is good news for an attacker, and bad
news for our server bills.

Gra*fast* prevents this from happening by forcing polymorphism to “fan in”
before it “fans out” again:

<figure>
  <Mermaid
    chart={`
%%{init: {'themeVariables': { 'fontSize': '12px'}}}%%
graph TD
  v["$nodeId<br>(e.g. 'User:1' or 'Organization:2')"]
  v-->|isUser?| GetUser[["Fetch user by id"]]
  v-->|isOrganization?| GetOrg[["Fetch organization by id"]]
  GetUser --> UserPets[/"getPetIds"\\]
  GetOrg --> OrgPets[/"getServiceAnimalIds"\\]
  UserPets & OrgPets -.-x Combined@{shape: docs, label: "Combined ids"}
  Combined-->Animal[["Fetch animal by id"]]
  Animal--> Cat
  Animal--> Dog
`}
  />
  
  <figcaption>
    <code>getPetIds</code> and <code>getServiceAnimals</code> “fan in” to one combined node in order
    to then fetch all of the Animals by their IDs. Once the IDs are fetched, the nodes “fan out” to the different
    Animal types.
  </figcaption>

</figure>

So for ten types (`P=10`) and ten levels of nested polymorphism (`D=10`)
instead of having up to 10,000,000,000 (`P^D`) branches; Gra*fast* scales linearly
with depth meaning there’s only up to 100 (`P*D`) branches.

Note we use "up to" because it's dependent on the shape of your plans. In our
fetch-to-type example above there would be no branching at all since all
abstract types use the same underlying step.

:::

### Advanced

If you want to be hyper-optimal, you may not want to always return a specifier
from your field plan resolvers — you may want to return and use a step that
already represents the relevant data that you already needed for some reason.
However, if this field is queried through multiple polymorphic paths Gra*fast*
will perform “fan in” to prevent excessive branching. To do so, it will
take the step returned from each of the relevant field plan resolvers and if
there’s more than one, it will do the first of these that yields results:

1. it will call the abstract type’s `toSpecifier($step)` method, if present, passing the step; or
2. it will call the step’s `$step.toRecord()` method, if present; or
3. it will use `$step`’s data directly.

Whichever of these it does, it will create a “combined data” step representing
**just the data** from these steps, yielded via the above options. This combined
step will then be input to `planType` as the specifier.

For consistency, Gra*fast* will also call `toSpecifier($step)` (if present) when there’s
only one step - this ensures the “specifier” step passed in to `planType` contains
consistent data whether there’s one step or many. However, if there was just one
step then it’ll also make available the original step via the `info` argument
(`info.$original`); you may use this if you want to be extra efficient somehow.

As shown in the [planType](#plantype) examples above, `toSpecifier` also goes in
the union or interface type’s extensions (`extensions.grafast.toSpecifier`); how
you would populate this depends on the framework you’re using; here’s an example
using `makeGrafastSchema`:

```ts
const schema = makeGrafastSchema({
  typeDefs: /* GraphQL */ `
    interface Animal {
      id: ID!
      #...
    }
  `,
  interfaces: {
    Animal: {
      toSpecifier($step) {
        // A simple data object with just the ID, perfect for accumulating
        return object({ id: get($step, "id") });
      },
      planType($obj, info) {
        // Extract the ID
        const $id = get($obj, "id");
        // Fetch the record (same for all types)
        const $record = loadOne($id, batchGetAnimalById);
        // Determine the GraphQL type name for the record
        const $__typename = $record.get("typename");
        // Return our polymorphic planner
        return {
          $__typename,
          planForType() {
            // We've already fetched the record, which is polymorphic, and we've
            // already determined the type; all types can thus share this same
            // step.
            return $record;
          },
        };
      },
    },
  },
});
```

## Caveats

Though Gra*fast* limits the impact of abstract types, they are still quite expensive
compared to regular types (a field returning an abstract type that could be N
different concrete types can be similarly complex to having N fields that return
concrete types). As such, to protect your servers from malicious queries we
recommend that you implement trusted documents if you can, and if not that you
apply an abstract type depth limit to avoid polymorphism’s branching becoming an
attack vector for your schema. Gra*fast* also makes available planning timeouts, but
these should be your fallback defence rather than your primary defence.
